import math

class Weight:
  def __init__(self, value):
    self.value = value

class Node:
  def __init__(self):
    self.inputs = []
    self.weights = []
    self.fixed_weight = None
    self.forward_neighbors = []
    self.forward_weights = []
    self.raw_value = 0
    self.transformed_value = 0

  def AddInput(self, node, weight, network):
    self.inputs.append(node)
    if not weight:
      weight = network.GetNewWeight()
    self.weights.append(weight)
    node.forward_neighbors.append(self)
    node.forward_weights.append(weight)
    if not self.fixed_weight:
      self.fixed_weight = network.GetNewWeight()

class Input:
  def __init__(self):
    self.values = []

class Target:
  def __init__(self):
    self.values = []

class NeuralNetwork:
  INPUT = 1
  HIDDEN = 2
  OUTPUT = 3

  def __init__(self):
    self.complete = False
    self.inputs = []
    self.hidden_nodes = []
    self.outputs = []
    self.node_set = {}
    self.weights = []

  def GetNewWeight(self):
    weight = Weight(0.0)
    self.weights.append(weight)
    return weight

  def AddNode(self, node, node_type):
    self.CheckIncomplete()
    if node_type == self.INPUT:
      assert len(node.inputs) == 0, 'Input node cannot have inputs'
    # Check that we only reference inputs already in the network
    for input in node.inputs:
      assert input in self.node_set, 'Cannot reference input that is not already in the network'
    self.node_set[node] = True
    if node_type == self.INPUT:
      self.inputs.append(node)
    elif node_type == self.HIDDEN:
      self.hidden_nodes.append(node)
    else:
      assert node_type == self.OUTPUT, 'Unexpected node_type: ' % node_type
      self.outputs.append(node)
    
  def MarkAsComplete(self):
    seen_nodes = {}
    for input in self.inputs:
      seen_nodes[input] = True
      assert len(input.inputs) == 0, 'Inputs should not have inputs of their own.'
    for node in self.hidden_nodes:
      seen_nodes[node] = True
      for input in node.inputs:
        assert input in seen_nodes, ('Node refers to input that was added to the network later than'
          'it.')
    for node in self.outputs:
      assert len(node.forward_neighbors) == 0, 'Output node cannot have forward neighbors.'
      for input in node.inputs:
        assert input in seen_nodes, ('Node refers to input that was added to the network later than'
          'it.')
    self.complete = True

  def CheckComplete(self):
    if self.complete:
      return
    self.MarkAsComplete()

  def CheckIncomplete(self):
    assert not self.complete, ('Tried to modify the network when it has already been marked as'
      'complete')

  @staticmethod
  def ComputeRawValue(node):
    total_weight = 0
    for i in range(len(node.inputs)):
      total_weight += node.weights[i].value * node.inputs[i].transformed_value
    total_weight += node.fixed_weight.value
    return total_weight
  
  @staticmethod
  def Sigmoid(value):
    try:
      return 1.0 / (1 + math.exp(-value))
    except:
      if value < 0:
        return 0.0
      else:
        return 1.0

  @staticmethod
  def SigmoidPrime(value):
    try:
      return math.exp(-value) / math.pow(1 + math.exp(-value), 2)
    except:
      return 0

  def InitFromWeights(self, weights):
    assert len(self.weights) == len(weights), (
      'Trying to initialize from a different sized weight vector.')
    for i in range(len(weights)):
      self.weights[i].value = weights[i]
